# util.py
import Image
import ImageFilter
import ImageDraw
import ImageChops
import hashlib
import math
import wx
import os,sys
import time
import pysage
import math
import cStringIO as StringIO

logger = pysage.get_logger()
shadow_cache = {}

def magnitude(v):
    return math.sqrt(v[0]**2 + v[1]**2)

def tail_recursive(g):
    '''
    Version of tail_recursion decorator using no stack-frame inspection.    
    '''
    loc_vars ={"in_loop":False,"cnt":0}

    def result(*args, **kwd):
        loc_vars["cnt"]+=1
        if not loc_vars["in_loop"]:
            loc_vars["in_loop"] = True
            while 1:            
                tc = g(*args,**kwd)
                try:                    
                    qual, args, kwd = tc
                    if qual == 'continue':
                        continue
                except (TypeError, ValueError):                    
                    loc_vars["in_loop"] = False
                    return tc                                    
        else:
            if loc_vars["cnt"]%2==0:
                return ('continue',args, kwd)
            else:
                return g(*args,**kwd)
    return result

if sys.platform.startswith('win'):
    get_time =  time.clock
else:
    get_time = time.time

def thumbnail(input, output):
    '''creates a thumbnail
       input: path to the incoming image file output: path to write to for the thumbnail'''
    size = 128,128
    im = Image.open(input)
    im.thumbnail(size)
    im.save(output, "JPEG")
    
def filter_files(files):
    '''given a list of files, filter them such that the remaining files are image files readable by PIL'''
    if len(files) == 0:
        return []
    else:
        ret = []
        for f in files:
            try:
                Image.open(f)
            except:
                continue
            else:
                ret.append(f)
        return ret
            
def hashv(value):
    h = hashlib.new("SHA512")
    h.update(value)
    return h.hexdigest()

class NotImplemented(Exception):
    pass

def scale_size(size, scale):
    return vec2(math.floor(size.x * scale), math.floor(size.y * scale) )

def max_size(image, maxSize, method = 3):
    '''im = maxSize(im, (maxSizeX, maxSizeY), method = Image.BICUBIC)
        Resizes a PIL image to a maximum size specified while maintaining
        the aspect ratio of the image.  Similar to Image.thumbnail(), but allows
        usage of different resizing methods and does NOT modify the image in place.
    '''
    imAspect = float(image.size[0])/float(image.size[1])
    outAspect = float(maxSize.x)/float(maxSize.y)
    if imAspect >= outAspect:
        #set to maxWidth x maxWidth/imAspect
        return image.resize((maxSize.x, int((float(maxSize.x)/imAspect) + 0.5)), method)
    else:
        #set to maxHeight*imAspect x maxHeight
        return image.resize((int((float(maxSize.y)*imAspect) + 0.5), maxSize.y), method)

def get_size_from_scale(size, scale):
    maxSize = BASE_THUMB_SIZE * scale
    imAspect = float(size.x)/float(size.y)
    outAspect = float(maxSize.x)/float(maxSize.y)
    if imAspect >= outAspect:
        return vec2(int(maxSize.x + 0.5), int((float(maxSize.x)/imAspect) + 0.5))
    else:
        return vec2(int((float(maxSize.y) * imAspect) + 0.5), int(maxSize.y + 0.5))
    
def trim_size(size, max):
    if size.x > max.x or size.y > max.y:
        aspect = float(size.x) / float(size.y)
        if size.x > size.y:
            return vec2(int(max.x), int(max.x / aspect))
        else:
            return vec2(int(max.y * aspect), int(max.y))
    return size
    
def get_best_size(image, maxSize):
    imAspect = float(image.size[0])/float(image.size[1])
    outAspect = float(maxSize[0])/float(maxSize[1])
    if imAspect >= outAspect:
        return vec2(maxSize[0], int((float(maxSize[0])/imAspect) + 0.5))
    else:
        return vec2(int((float(maxSize[1])*imAspect) + 0.5), maxSize[1])
    
def pil_to_bitmap(img):
    '''converts a pil image to a wxpython usable bitmap'''
    return wx.BitmapFromBuffer(img.size[0], img.size[1], img.convert('RGB').tostring(), img.convert('RGBA').tostring()[3::4])

def pil_to_image(img):
    '''converts a pil image to a wxpython usable bitmap'''
    image = wx.EmptyImage(img.size[0], img.size[1])
    image.SetData(img.convert('RGB').tostring())
    image.SetAlphaData(img.convert('RGBA').tostring()[3::4])
    return image

def string_to_image(data, size):
    image = wx.EmptyImage(size[0], size[1])
    image.SetData(data[0])
    image.SetAlphaData(data[1])
    return image

def get_image_block_size(quant):
    '''returns a 2 tuple specifying the size of the image block based the amount of images to display
        on a 8 x 6 ratio
    '''
    hsize = int(math.sqrt( quant * 8.0 / 6.0))
    return (hsize, int(math.ceil(float(quant) / hsize)))

def get_image_location(ind, block_hsize, block_vsize, max_img_hsize, max_img_vsize, h_offset=0, v_offset=0, border=0):
    '''given: max size of the image, the hsize and vsize of the block, and the index of the image
       return: calculate the location of the image
    '''
    h_loc = ind % block_hsize * (max_img_hsize + border)
    v_loc = 0 if ind == 0 else ind / block_hsize * (max_img_vsize + border)
    return vec2(h_loc + h_offset, v_loc + v_offset)

def user_data_dir(appname, owner=None, version=None):
    '''Return full path to the user-specific data dir for this application.
    
        "appname" is the name of application.
        "owner" (only required and used on Windows) is the name of the
            owner or distributing body for this application. Typically
            it is the owning company name.
        "version" is an optional version path element to append to the
            path. You might want to use this if you want multiple versions
            of your app to be able to run independently. If used, this
            would typically be "<major>.<minor>".
    
    Typical user data directories are:
        Windows:    C:\Documents and Settings\USER\Application Data\<owner>\<appname>
        Mac OS X:   ~/Library/Application Support/<appname>
        Unix:       ~/.<lowercased-appname>
    Copyright (c) 2005 ActiveState Corp.
    License: MIT
    Author:  Trent Mick (TrentM at ActiveState.com)
    '''
    if sys.platform.startswith("win"):
        # Try to make this a unicode path because SHGetFolderPath does
        # not return unicode strings when there is unicode data in the
        # path.
        if owner is None:
            raise Exception("must specify 'owner' on Windows")
        from win32com.shell import shellcon, shell
        path = shell.SHGetFolderPath(0, shellcon.CSIDL_APPDATA, 0, 0)
        try:
            path = unicode(path)
        except UnicodeError:
            pass
        path = os.path.join(path, owner, appname)
    elif sys.platform == 'darwin':
        from Carbon import Folder, Folders
        path = Folder.FSFindFolder(Folders.kUserDomain, Folders.kApplicationSupportFolderType, Folders.kDontCreateFolder)
        path = os.path.join(path.FSRefMakePath(), appname)
    else:
        path = os.path.expanduser("~/." + appname.lower())
    if version:
        path = os.path.join(path, version)
    return path

def site_data_dir(appname, owner=None, version=None):
    '''Return full path to the user-shared data dir for this application.
    
        "appname" is the name of application.
        "owner" (only required and used on Windows) is the name of the
            owner or distributing body for this application. Typically
            it is the owning company name.
        "version" is an optional version path element to append to the
            path. You might want to use this if you want multiple versions
            of your app to be able to run independently. If used, this
            would typically be "<major>.<minor>".
    
    Typical user data directories are:
        Windows:    C:\Documents and Settings\All Users\Application Data\<owner>\<appname>
        Mac OS X:   /Library/Application Support/<appname>
        Unix:       /etc/<lowercased-appname>
    Copyright (c) 2005 ActiveState Corp.
    License: MIT
    Author:  Trent Mick (TrentM at ActiveState.com)
    '''
    if sys.platform.startswith("win"):
        # Try to make this a unicode path because SHGetFolderPath does
        # not return unicode strings when there is unicode data in the
        # path.
        if owner is None:
            raise Exception("must specify 'owner' on Windows")
        from win32com.shell import shellcon, shell
        shellcon.CSIDL_COMMON_APPDATA = 0x23 # missing from shellcon
        path = shell.SHGetFolderPath(0, shellcon.CSIDL_COMMON_APPDATA, 0, 0)
        try:
            path = unicode(path)
        except UnicodeError:
            pass
        path = os.path.join(path, owner, appname)
    elif sys.platform == 'darwin':
        from Carbon import Folder, Folders
        path = Folder.FSFindFolder(Folders.kLocalDomain, Folders.kApplicationSupportFolderType, Folders.kDontCreateFolder)
        path = os.path.join(path.FSRefMakePath(), appname)
    else:
        path = "/etc/"+appname.lower()
    if version:
        path = os.path.join(path, version)
    return path

def get_box_center(bbox):
    x = (bbox[1][0] - bbox[0][0]) * .5 + bbox[0][0]
    y = (bbox[1][1] - bbox[0][1]) * .5 + bbox[0][1]
    return vec2(x, y)

def process_image(data):
    im = Image.open(StringIO.StringIO(data))
#    return drop_shadow(im, horizontal_offset=50, vertical_offset=50, background_colour=(255,255,255,0),
#                shadow_colour=0x444444, border=80, shadow_blur=30,
#                force_background_colour=False, cache=shadow_cache)
    # return drop_shadow(im, shadow_blur = 3.0 * math.log(2.0 + scale), border=8.0 * scale, vertical_offset=1.0 * scale, horizontal_offset=1.0 * scale, force_background_colour=False)
    return pil_to_image(drop_shadow(im, shadow_blur = 50, border=22, 
                       vertical_offset=25, horizontal_offset=25, 
                       force_background_colour=False, shadow_colour=0x444444, cache=shadow_cache))
    
#    data = StringIO.StringIO()
#    drop_shadow(im, shadow_blur = 50, border=22, 
#                       vertical_offset=25, horizontal_offset=25, 
#                       force_background_colour=False, shadow_colour=0x444444, cache=shadow_cache).save(data, 'PNG')
#    return wx.ImageFromStream(StringIO.StringIO(data.getvalue()))

def fancy_thumbnail(image, size, imageid, enforce_size=True, use_bitmap=False):
    '''returns a rounded corner image with drop shadow'''
    logger.debug('image "%s": fancy thumbnail received size "%s"' % (imageid, size))
    # make sure size is no larger than the original size
    if size.x > image.size[0]:
        logger.info('image "%s": requested size larger than original size "%s" "%s"' % (imageid, size, image.size))
        size = vec2(image.size[0], image.size[1])
    orig_size = size        
    image = max_size(image, size)
    result = image
    # return max_size(drop_shadow(image, shadow_blur = 3.0 * math.log(2.0 + scale), border=8.0 * scale, vertical_offset=1.0 * scale, horizontal_offset=1.0 * scale, force_background_colour=false), size)
    size = vec2(image.size[0], image.size[1])
    
    logger.debug('image "%s": fancy thumbnail max_size to "%s"' % (imageid, size))
    scale = float(size.x) / float(BASE_THUMB_SIZE.x)
    logger.debug('image "%s": producing fancy thumbnail of size "%s"' % (imageid, str(size)))
    # return max_size(drop_shadow(image, shadow_blur = 3.0 * math.log(2.0 + scale), border=8.0 * scale, vertical_offset=1.0 * scale, horizontal_offset=1.0 * scale, force_background_colour=False), size)
    result =  drop_shadow(image, shadow_blur = 3.0 * math.log(2.0 + scale), border=8.0 * scale, vertical_offset=1.0 * scale, horizontal_offset=1.0 * scale, force_background_colour=False, paste_original=True)
    if enforce_size:
        result = result.resize((orig_size.x, orig_size.y), Image.ANTIALIAS)
    else:
        result = result.resize((size.x, size.y), Image.ANTIALIAS)
    logger.debug('image "%s": fancy thumbnail returning size "%s"' % (imageid, str(result.size)))
    if use_bitmap:
        return pil_to_bitmap(result)
    return pil_to_image(result)
    # return drop_shadow(round_image(max_size(image, size), radius = 2.0 * scale), shadow_blur = 3.0 * math.log(2.0 + scale), border=8.0 * scale, vertical_offset=1.0 * scale, horizontal_offset=1.0 * scale, force_background_colour=False)
    
def image_to_pil(image):
    pil = Image.new('RGB', (image.GetWidth(), image.GetHeight()))
    pil.fromstring(image.GetData())
    return pil
    
def back_drop(img, horizontal_offset=25, vertical_offset=25, background_colour=(255,255,255,0), 
                shadow_colour=0x444444, border=22, shadow_blur=500, 
                force_background_colour=False, cache={}):
    size = img.GetSize()
    #create cache id
    id  = ''.join([str(x) for x in ['shadow_',size,horizontal_offset,vertical_offset,
                border, shadow_blur,background_colour,shadow_colour]])
    if cache.has_key(id):
        return cache[id]
    else:
#        back = drop_shadow(image_to_pil(img), horizontal_offset=0, vertical_offset=0, background_colour=(255,255,255,0), 
#                shadow_colour=0x444444, border=10, shadow_blur=120, 
#                force_background_colour=False, cache={})
        back = ratio_to_shadow(float(size[0]) / float(size[1]))
        cache[id] = pil_to_image(back)
        return cache[id]
    
def ratio_to_shadow(ratio):
    scale = 1.0
    shadow_blur = 3.0 * math.log(2.0 + scale)
    # shadow_blur = 1
    border=8.0 * scale
    vertical_offset=1.0 * scale
    horizontal_offset=1.0 * scale
    shadow_colour=0x444444
    
    size = (int(ratio * SHADOW_HEIGHT ), SHADOW_HEIGHT)
    back_size  = (size[0] + abs(horizontal_offset) + 2*border,
                  size[1] + abs(vertical_offset) + 2*border)
    image_mask  = Image.new('RGBA', size, shadow_colour)
    shadow      = Image.new('L',back_size,0)
    shadow_left     = border + max(horizontal_offset, 0)
    shadow_top      = border + max(vertical_offset, 0)
    shadow.paste(image_mask, (shadow_left, shadow_top, shadow_left + size[0], shadow_top + size[1]))
    del image_mask
    
    n   = 0
    while n < shadow_blur:
        shadow = shadow.filter(ImageFilter.BLUR)
        n += 1
        
    back  = Image.new('RGBA',back_size,shadow_colour)
    back.putalpha(shadow)
    del shadow #free up memory
    return back

def drop_shadow(image, horizontal_offset=5, vertical_offset=5, background_colour=(255,255,255,0), 
                shadow_colour=0x444444, border=8, shadow_blur=3, 
                force_background_colour=False, cache={}, paste_original=False):
    """ 
    Add a gaussian blur drop shadow to an image.  

    image           - The image to overlay on top of the shadow.
    offset          - Offset of the shadow from the image as an (x,y) tuple.  
                        Can be positive or negative.
    background_colour     - Background colour behind the image.
    shadow_colour   - Shadow colour (darkness).
    border          - Width of the border around the image.  This must be wide
                        enough to account for the blurring of the shadow.
    shadow_blur      - Number of times to apply the filter.  More shadow_blur 
                        produce a more blurred shadow, but increase processing 
                        time.
    """ 
    #get info
    size        = image.size
    mode        = image.mode
           
    back        = None
                
    #assert image is RGBA
    if mode != 'RGBA':
#        if mode != 'RGB':
#            image   = image.convert('RGB')
#            mode    = 'RGB'
        image = image.convert('RGBA')
        mode = image.mode
        
        #create cache id
        id  = ''.join([str(x) for x in ['shadow_',size,horizontal_offset,vertical_offset,
                    border, shadow_blur,background_colour,shadow_colour]])
                        
        #look up in cache
        if cache.has_key(id):
            #retrieve from cache
            back, back_size = cache[id]
                
    if back is None:
        #size of backdrop
        back_size       = (size[0] + abs(horizontal_offset) + 2*border,
                        size[1] + abs(vertical_offset) + 2*border)

        #create shadow mask
        if mode == 'RGBA':
            image_mask  = image.split()[-1]
            shadow      = Image.new('L',back_size,0)
        else:
            image_mask  = Image.new(mode,size,shadow_colour)
            shadow      = Image.new(mode,back_size,background_colour)
                
        shadow_left     = border + max(horizontal_offset, 0)
        shadow_top      = border + max(vertical_offset, 0)
        shadow.paste(image_mask, (shadow_left, shadow_top, 
                                shadow_left + size[0], shadow_top + size[1]))
        del image_mask #free up memory
                                
        #blur shadow mask
        
        #Apply the filter to blur the edges of the shadow.  Since a small
        #kernel is used, the filter must be applied repeatedly to get a decent
        #blur.
        n   = 0
        while n < shadow_blur:
            shadow = shadow.filter(ImageFilter.BLUR)
            n += 1

        #create back
        if mode == 'RGBA':
            back  = Image.new('RGBA',back_size,shadow_colour)
            back.putalpha(shadow)
            del shadow #free up memory
        else:
            back        = shadow

        cache[id]   = back, back_size
    #Paste the input image onto the shadow backdrop
    if paste_original:
        image_left  = border - min(horizontal_offset, 0)
        image_top   = border - min(vertical_offset, 0)
        if mode == 'RGBA':
            back.paste(image, (image_left, image_top),image)
            if force_background_colour:
                mask    = back.split()[-1]
                back.paste(Image.new('RGB',back.size,background_colour),(0,0),
                    ImageChops.invert(mask))
                back.putalpha(mask)
        else:
            back.paste(image, (image_left, image_top))   

    return back

CROSS                   = 'Cross'
ROUNDED                 = 'Rounded'
SQUARE                  = 'Square'

CORNERS                 = [ROUNDED,SQUARE,CROSS]
CORNER_ID               = 'rounded_corner_r%d_f%d'
CROSS_POS               = (CROSS,CROSS,CROSS,CROSS)
ROUNDED_POS             = (ROUNDED,ROUNDED,ROUNDED,ROUNDED)
ROUNDED_RECTANGLE_ID    = 'rounded_rectangle_r%d_f%d_s%s_p%s'
 
def round_image(image,cache={},radius=100,opacity=255,pos=ROUNDED_POS,
        back_colour='#FFFFFF'):
    if image.mode != 'RGBA':
        image = image.convert('RGBA')
    mask    = create_rounded_rectangle(image.size,cache,radius,opacity,pos)
    image.paste(Image.new('RGB',image.size,back_colour),(0,0),
        ImageChops.invert(mask))
    image.putalpha(mask)
    return image

def create_corner(radius=100,opacity=255,factor=2):
    corner  = Image.new('L',(factor*radius,factor*radius),0)
    draw    = ImageDraw.Draw(corner)
    draw.pieslice((0,0,2*factor*radius,2*factor*radius),180,270,fill=opacity)
    corner  = corner.resize((radius,radius),Image.ANTIALIAS)
    return corner

def create_rounded_rectangle(size=(600,400),cache={},radius=100,opacity=255,
        pos=ROUNDED_POS):
    #rounded_rectangle
    im_x, im_y  = size
    rounded_rectangle_id    = ROUNDED_RECTANGLE_ID%(radius,opacity,size,pos)
    if cache.has_key(rounded_rectangle_id):
        return cache[rounded_rectangle_id]
    else:
        #cross  
        cross_id    = ROUNDED_RECTANGLE_ID%(radius,opacity,size,CROSS_POS)
        if cache.has_key(cross_id):
            cross   = cache[cross_id]
        else:
            cross   = cache[cross_id]   = Image.new('L',size,0)
            draw    = ImageDraw.Draw(cross) 
            draw.rectangle((radius,0,im_x-radius,im_y),fill=opacity)
            draw.rectangle((0,radius,im_x,im_y-radius),fill=opacity)
        if pos==CROSS_POS:
            return cross
        #corner
        corner_id   = CORNER_ID%(radius,opacity)
        if cache.has_key(corner_id):
            corner  = cache[corner_id]
        else:
            corner  = cache[corner_id] = create_corner(radius,opacity)
        #rounded rectangle
        rectangle   = Image.new('L',(radius,radius),255)
        rounded_rectangle   = cross.copy()
        for index, angle in enumerate(pos):
            if angle == CROSS:
                break
            if angle == ROUNDED:
                element = corner
            else:
                element = rectangle
            if index%2:
                x       = im_x-radius
                element = element.transpose(Image.FLIP_LEFT_RIGHT)
            else:
                x       = 0
            if index < 2:
                y       = 0
            else:
                y       = im_y-radius
                element = element.transpose(Image.FLIP_TOP_BOTTOM)
            rounded_rectangle.paste(element,(x,y))
        cache[rounded_rectangle_id] = rounded_rectangle
        return rounded_rectangle
    
class vec2(object):
    __slots__ = ('x', 'y')
    def __init__(self, x=0.0, y=0.0):
            self.x = x
            self.y = y
    def __str__(self):
            return '(%.1f,%.1f)' % (self.x, self.y)
    def __repr__(self):
            return str(self)
    def __eq__(self, o):
            return self.x == o.x and self.y == o.y
    def __cmp__(self, o):
            return self.__eq__(o)
    def __ne__(self, o):
            return not self.__eq__(o)
    def __hash__(self):
            return self.x + self.y * 10000
    def __lt__(self, o):
            if self.x < o.x:
                    return True
            if self.y < o.y:
                    return True
    def __add__(self, o):
            return vec2(self.x + o.x, self.y + o.y)
    def __sub__(self, o):
            return vec2(self.x - o.x, self.y - o.y)
    def __div__(self, o):
            return vec2(self.x / o, self.y / o)
    def __mul__(self, o):
            return vec2(self.x * o, self.y * o)
    def __neg__(self):
            return vec2(-self.x, -self.y)
        
BASE_THUMB_SIZE = vec2(80, 60)
ALBUM_THUMB_SIZE = vec2(60, 45)
# beyond this point, the image will not have higher resolution in the render window
MAX_RENDER_SIZE = vec2(800,800)
SHADOW_HEIGHT = 100

def get_new_viewport(point, old_scale, offset, new_scale):
    '''given the point on screen, the old scale, the original viewport, and the new scale
        calculate the new viewport
    '''
    # where the mouse is focused in image space in original zoom level
    #focus_oldscale = orig_viewport + (point * 1.0 / old_scale)
    # where the mouse would be in the new zoom levelx
    #focus_newscale = orig_viewport + (point * 1.0 / new_scale)
    
    # this is how much the current focus differs from the new focus
    #delta = focus_newscale - focus_oldscale
    
    # the new viewport is the original viewport moved such that the new and old focus are at the same point
    
    # new_origin = offset - (point - offset ) * (1 - new_scale / old_scale)
    # return new_origin
    # return offset + point - (point * (new_scale / old_scale ))
    # print point, new_scale, old_scale
    return (offset - point) * (new_scale / old_scale) + point

def draw_location(loc, offset, scale):
    return loc * scale + offset

#def collide(box1, box2):
#    loc1, size1 = box1
#    loc2, size2 = box2
#    
#    left1 = loc1.x
#    left2 = loc2.x
#    right1 = loc1.x + size1.x
#    right2 = loc2.x + size2.x
#    
#    top1 = loc1.y + size1.y
#    top2 = loc2.y + size2.y
#    bottom1 = loc1.y
#    bottom2 = loc2.y
#    
#    if bottom1 > top2:
#        return False
#    if top1 < bottom2:
#        return False
#    if left1 > right2:
#        return False
#    if right1 < left2:
#        return False
#    
#    return True

def collide(BB1, BB2):
    return (BB1[1,0] > BB2[0,0] and BB1[0,0] < BB2[1,0] and
        BB1[1,1] > BB2[0,1] and BB1[0,1] < BB2[1,1])
    
    
    

    
    
 
 
